/*

 Copyright 2012 Peter Dornbach.

 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
   
*/

package com.dornbachs.zebra.guitarear;

import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.Vector;

public class ChordShape implements Cloneable {
	public static int toMidiNote(String noteFullName) {
		int octaveStart = noteFullName.length() - 1;
		int octave = noteFullName.charAt(octaveStart) - '0';
		String noteName = noteFullName.substring(0, octaveStart).toUpperCase();
		if (noteName.endsWith("SHARP")) {
			noteName = noteName.substring(0, noteName.length() - 5) + "#";
		}
		int note = NOTE_NAME_MAP.get(noteName);
		return note + (octave + 1) * 12;
	}
	
	public static final String removeOctave(String note) {
		if (note.length() > 1) {
			if (note.charAt(1) == '#') {
				return note.substring(0, 2);
			}
		}
		return note.substring(0, 1);
	}
	
	
	public static final Vector<ChordShape> randomChordShapes(
			ChordShape[] chordShapesByLevel,
			int maxLevel,
			int numChords,
			Vector<Vector<ChordShape>> previousRiffChords) {
		// Compute a set of previous chords first to avoid much overlap if possible.
		HashSet<String> previousChords = new HashSet<String>();
		for (Vector<ChordShape> i : previousRiffChords) {
			for (ChordShape c : i) {
				previousChords.add(c._name);
			}
		}
		
		String[] scale = pickScale(chordShapesByLevel, previousChords, maxLevel, numChords);
		
		// Create the chord shapes.
		Vector<ChordShape> result = new Vector<ChordShape>();
		for (int i = 0; i < numChords; ++i) {
			ChordShape candidate = null;
			while (true) {
				candidate = randomChordShape(chordShapesByLevel, maxLevel);
				if (result.contains(candidate)) {
					continue;
				}
				if (!candidate._compatibleScales.contains(scale)) {
					continue;
				}
				if (previousChords.contains(candidate._name)) {
					if (Math.random() < 0.9) {
						continue;
					}
				}
				break;
			}
			result.add(candidate);
		}
		return result;
	}
	
	public static final ChordShape randomChordShape(
			ChordShape[] chordShapesByLevel, int maxLevel) {
		return chordShapesByLevel[Util.random(NUM_CHORDS_PER_LEVEL[maxLevel])];
	}
	
	public ChordShape(String name, int level, String[] notes) {
		_name = name;
		_level = level;
		_notes = notes;

		// Compute all notes for this chord.
		HashSet<String> chordNotes = new HashSet<String>();
		for (String note: notes) {
			chordNotes.add(removeOctave(note));
		}
		
		// Compute all compatible scales.
		_compatibleScales = new Vector<String[]>();
		for (String[] scale: ALL_SCALES) {
			boolean compatible = true;
			for (String note: chordNotes) {
				if (!Arrays.asList(scale).contains(note)) {
					compatible = false;
					break;
				}
			}
			if (compatible) {
				_compatibleScales.add(scale);
			}
		}
	}
	
	public ChordShape clone() {
		return new ChordShape(_name, _level, _notes);
	}
	
	public String getQuality() {
		String postfix = "";
		if (_name.length() > 1) {
			postfix = (_name.charAt(1) == '#' ? _name.substring(2) : _name.substring(1));
		}
		if (postfix.length() == 0) return "maj";
		if (postfix.equals("m")) return "min";
		return postfix;
	}
	
	public String _name;
	public int _level;
	public String[] _notes;
	public Vector<String[]> _compatibleScales;

	private static final HashMap<String, Integer> NOTE_NAME_MAP;	
	private static final Vector<String[]> ALL_SCALES;

	static {
		final String[] NOTE_NAMES = { "C", "C#", "D", "D#", "E", "F", "F#", "G",
				"G#", "A", "A#", "B" };
		
		NOTE_NAME_MAP = new HashMap<String, Integer>();
		ALL_SCALES = new Vector<String[]>();
		
		for (int i = 0; i < NOTE_NAMES.length; ++i) {
			NOTE_NAME_MAP.put(NOTE_NAMES[i], i);
		}

		// The minor scale is the same as the major except that it starts on
		// a different note.
		final int[] MAJOR_SCALE = { 0, 2, 4, 5, 7, 9, 11 };

		for (int i = 0; i < NOTE_NAMES.length; ++i) {
      String[] scaleNotes = new String[MAJOR_SCALE.length];			
			for (int j = 0; j < MAJOR_SCALE.length; ++j) {
				scaleNotes[j] = NOTE_NAMES[(i + MAJOR_SCALE[j]) % NOTE_NAMES.length];
			}
			ALL_SCALES.add(scaleNotes);
		}
	}

  public final static ChordShape[] FULL_BY_LEVEL = {
		new ChordShape("A",     0,  new String[]{"E4",  "A4",  "E5",  "A5",  "C#6", "E6" }),
		new ChordShape("D",     0,  new String[]{       "A4",  "D5",  "A5",  "D6",  "F#6"}),
		new ChordShape("E",     0,  new String[]{"E4",  "B4",  "E5",  "G#5", "B5",  "E6" }),
		
		new ChordShape("Am",    1,  new String[]{"E4",  "A4",  "E5",  "A5",  "C6",  "E6" }),
		new ChordShape("Dm",    1,  new String[]{       "A4",  "D5",  "A5",  "D6",  "F6" }),
		new ChordShape("Em",    1,  new String[]{"E4",  "B4",  "E5",  "G5",  "B5",  "E6" }),
		
		new ChordShape("C",     2,  new String[]{       "C5",  "E5",  "G5",  "C6",  "E6" }),
		new ChordShape("G",     2,  new String[]{"G4",  "B4",  "D5",  "G5",  "B5",  "G6" }),

		new ChordShape("B7",    3,  new String[]{       "B4",  "D#5", "A5",  "B5",  "F#6"}),
		new ChordShape("C7",    3,  new String[]{       "C5",  "E5",  "A#5", "C6",  "E6" }),
		new ChordShape("G7",    3,  new String[]{"G4",  "B4",  "D5",  "G5",  "B5",  "F6" }),

		new ChordShape("A7",    4,  new String[]{       "A4",  "E5",  "G5",  "C#6", "E6" }),
		new ChordShape("D7",    4,  new String[]{       "A4",  "D5",  "A5",  "C6",  "F#6"}),
		new ChordShape("E7",    4,  new String[]{"E4",  "B4",  "D5",  "G#5", "B5",  "E6" }),

		new ChordShape("F",     5,  new String[]{"F4",  "C5",  "F5",  "A5",  "C6",  "F6" }),
		new ChordShape("Fmaj7", 5,  new String[]{       "A4",  "F5",  "A5",  "C6",  "E6" }),

		new ChordShape("Asus2", 6,  new String[]{       "A4",  "E5",  "A5",  "B5",  "E6" }),
		new ChordShape("Asus4", 6,  new String[]{       "A4",  "E5",  "A5",  "D6",  "E6" }),

		new ChordShape("Dsus2", 7,  new String[]{       "A4",  "D5",  "A5",  "D6",  "E6" }),
		new ChordShape("Dsus4", 7,  new String[]{       "A4",  "D5",  "A5",  "D6",  "G6" }),

		new ChordShape("B",     8,  new String[]{"F#4", "B4",  "F#5", "B5",  "D#6", "F#6"}),
		new ChordShape("Bm",    8,  new String[]{"F#4", "B4",  "F#5", "B5",  "D6",  "F#6"}),

		new ChordShape("A5",    9,  new String[]{       "A4",  "E5",  "A5",              }),
		new ChordShape("D5",    9,  new String[]{              "D5",  "A5",  "D6"        }),
		new ChordShape("E5",    9,  new String[]{"E4",  "B4",  "E5",                     }),

		new ChordShape("C5",    10, new String[]{       "C5",  "G5",  "C6",              }),
		new ChordShape("G5",    10, new String[]{"G4",         "D5",  "G5",  "D6",  "G6" }),

		new ChordShape("B5",    11, new String[]{       "B5",  "F#5", "B6",              }),
		new ChordShape("F5",    11, new String[]{"F4",  "C5",  "F5",                     }),
	};

	public final static int MAX_LEVEL = FULL_BY_LEVEL[FULL_BY_LEVEL.length - 1]._level;
	private final static int NUM_CHORDS_PER_LEVEL[] = new int[MAX_LEVEL + 1];
  
  public final static ChordShape[] REDUCED_BY_LEVEL;
	public final static HashMap<String, ChordShape> FULL_BY_NAME;
	public final static HashMap<String, ChordShape> REDUCED_BY_NAME;
	
	// Key is "chord1:chord2".
	//private static final HashMap<String, Float> SOUND_SCORE_MAP;
	
	static {
		int previousLevel = 0;
		for (int i = 0; i < FULL_BY_LEVEL.length; ++i) {
			if (FULL_BY_LEVEL[i]._level > previousLevel) {
				NUM_CHORDS_PER_LEVEL[previousLevel] = i;
				previousLevel = FULL_BY_LEVEL[i]._level;
			}
		}
		NUM_CHORDS_PER_LEVEL[MAX_LEVEL] = FULL_BY_LEVEL.length;
	}
	
	static {
		REDUCED_BY_LEVEL = new ChordShape[FULL_BY_LEVEL.length];
		for (int i = 0; i < REDUCED_BY_LEVEL.length; ++i) {
			REDUCED_BY_LEVEL[i] = FULL_BY_LEVEL[i].clone();
			REDUCED_BY_LEVEL[i]._notes = reduce(FULL_BY_LEVEL[i]._name, FULL_BY_LEVEL[i]._notes);
		}
		
		FULL_BY_NAME = new HashMap<String, ChordShape>();
		REDUCED_BY_NAME = new HashMap<String, ChordShape>();
		for (int i = 0; i < FULL_BY_LEVEL.length; ++i) {
			FULL_BY_NAME.put(
					FULL_BY_LEVEL[i]._name,
					FULL_BY_LEVEL[i]);
			REDUCED_BY_NAME.put(
					REDUCED_BY_LEVEL[i]._name,
					REDUCED_BY_LEVEL[i]);
		}
	}

	private static final int get(HashMap<String, Integer> map, String key) {
		Integer value = map.get(key);
		return (value == null ? 0 : value);
	}
	
	// Keeps one note from each type. Used for distorted sounds.
	private static final String[] reduce(String name, String[] notes) {
	  Vector<String> result = new Vector<String>(6);
    HashMap<String, Integer> freqMap = new HashMap<String, Integer>();
    String root = removeOctave(name);
    boolean seenRoot = false;
	  for (String noteWithOctave: notes) {
  		String note = removeOctave(noteWithOctave);
	  	if (seenRoot) {
  			int freq = get(freqMap, note);
	  		if (freq < 1 || (note.equals(root) && freq < 2)) {
	  			result.add(noteWithOctave);
	  			freqMap.put(note, freq + 1);
	  		}
	  	} else {
	  		if (note.equals(root)) {
	  			result.add(noteWithOctave);
	  			freqMap.put(note, 1);
	  			seenRoot = true;
	  		}
	  	}
	  }
	  return (String[])result.toArray(new String[result.size()]);
	}

	private static final String[] pickScale(
			ChordShape[] chordShapesByLevel,
			Set<String> previousChords,
			int maxLevel,
			int numChords) {
		// Find the compatible scales.
		HashMap<String[], Integer> scaleCount = new HashMap<String[], Integer>();
		HashMap<String[], Integer> scaleScore = new HashMap<String[], Integer>();
		for (int i = 0; i < NUM_CHORDS_PER_LEVEL[maxLevel]; ++i) {
			// Chords not in the previous riff have a higher score.
			final int chordScore = (previousChords.contains(chordShapesByLevel[i]) ? 1 : 4);
			for (String[] scale: chordShapesByLevel[i]._compatibleScales) {
				if (scaleCount.containsKey(scale)) {
					scaleCount.put(scale, scaleCount.get(scale) + 1);
					scaleScore.put(scale, scaleScore.get(scale) + chordScore);
				} else {
					scaleCount.put(scale, 1);
					scaleScore.put(scale, chordScore);
				}
			}
		}

		// Pick a favorite scale. The probability of each one is proportional to
		// the number of possible chord combinations with that scale.
		double maxCumulativeScaleProbability = 0.0;
		Vector<String[]> scales = new Vector<String[]>();
		Vector<Double> scaleCumulativeProbability = new Vector<Double>();
		for (String[] scale: scaleCount.keySet()) {
			final int c = scaleCount.get(scale);
			if (c >= numChords) {
				double p = Util.combination(c, numChords);
				p *= scaleScore.get(scale) / scaleCount.get(scale);
				maxCumulativeScaleProbability += p;
				scales.add(scale);
				scaleCumulativeProbability.add(maxCumulativeScaleProbability);
			}
		}
		
		assert(scales.size() > 0);

		final double r = Math.random() * maxCumulativeScaleProbability;
		for (int i = 0; i < scales.size(); ++i) {
		  if (r < scaleCumulativeProbability.get(i)) {
		  	return(scales.get(i));
		  }
		}
		return scales.lastElement();
	}
}